Skip to content

[FEATURE](schema) Add IntegerConstraint; fail codegen on divergent union column types#553

Draft
Seth Fitzsimmons (sethfitz) wants to merge 4 commits into
pyspark-codegenfrom
union-type-compat
Draft

[FEATURE](schema) Add IntegerConstraint; fail codegen on divergent union column types#553
Seth Fitzsimmons (sethfitz) wants to merge 4 commits into
pyspark-codegenfrom
union-type-compat

Conversation

@sethfitz

@sethfitz Seth Fitzsimmons (sethfitz) commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #472.

Implements the solution agreed in the issue thread, on top of the PySpark codegen rewrite:

  1. New IntegerConstraint field constraint: validates integral floats in Pydantic, emits multipleOf: 1 in JSON Schema, and dispatches to a new check_integer PySpark expression with generated conformance-test coverage.
  2. VehicleAxleCountSelector.value becomes float64 with IntegerConstraint and a minimum of 1 (was uint8).
  3. The codegen now fails on any same-named union field whose arms resolve to different Spark types.
  4. The numeric widening machinery is removed. The generated Segment schema is unchanged (the value column was already DoubleType via widening; it is now DoubleType natively), with new arm-gated integer/bounds checks.

Union arms that share a field name and type but diverge in constraints now produce separate arm-gated checks instead of an extraction error; violation labels for a split field's checks are suffixed by arm (value_0, value_1), and generation fails loudly if two same-name checks ever land in one arm.

Introduce IntegerConstraint, a field constraint that requires a
floating-point value be a whole number. It validates in Pydantic
(rejecting non-integral floats), emits multipleOf: 1 in JSON Schema, and
dispatches to a new check_integer PySpark expression. The constraint
lets a count be modeled as float64 (for a uniform Parquet column) while
still rejecting fractional values.

Wire it through the codegen pipeline: constraint dispatch, the
check_integer runtime expression, invalid-value generation for
conformance tests, and markdown constraint description. Base-row
synthesis needs no change -- the accompanying bounds drive the value.

Refs #472

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
… float64

VehicleAxleCountSelector.value was the only VehicleSelector arm typed
uint8; the other four use float64. Flattened to a Spark struct, all five
arms share one `value` column -- the codegen widened uint8 to DoubleType
to make that column work, an implicit compatibility requirement nothing
in the model surfaced.

Change value to float64 with IntegerConstraint (must be a whole number)
and a minimum of 1. This makes the union's Spark type explicit rather
than implicit: `value` was already DoubleType via widening, and is now
DoubleType natively.

Generalizing schema_builder and union_extraction (previous commit) is a
prerequisite: without it, this change fails at generation with a
diverging-constraints error, since axle count's `ge=1 + integral` rule
differs from the other arms' `ge=0`.

Regenerate the PySpark modules. The only changes are to Segment (the
sole model embedding VehicleSelector): new `bounds` (ge=1.0) and
`integer` checks gated to the axle_count arm, new conformance-test
scenarios exercising them, and check-function/label renumbering from the
added collision in the `value` group. No other model tripped the
generalized union type-divergence check.

Refs #472

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
@sethfitz Seth Fitzsimmons (sethfitz) added the change type - minor 🤏 Minor schema change. See https://lf-overturemaps.atlassian.net/wiki/x/GgDa label Jul 10, 2026
@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

🗺️ Schema reference docs preview is live!

🌍 Preview https://staging.overturemaps.org/schema/pr/553/schema/index.html
🕐 Updated Jul 10, 2026 23:28 UTC
📝 Commit d029544
🔧 env SCHEMA_PREVIEW true

Note

♻️ This preview updates automatically with each push to this PR.

…lit-field labels by arm

check_integer tested `col != F.floor(col)`. F.floor casts to LongType
and saturates doubles at 2^63, so integral values like 1e30 and 9.9e18
failed the check even though Pydantic accepts them (float.is_integer()
is True).  Test the fractional part in double space instead (`col % 1 !=
0`): those large integral doubles pass, and 2.5, NaN, and infinity fail
-- matching float.is_integer(). Correct the docstring, which claimed
floor "passes" NaN; floor(NaN) returns 0, so the old code rejected NaN
through a false mechanism.

Group field-check label collision suffixes by union arm rather than by
(label, name) occurrence. A check present in only one arm -- the axle
arm's integer check, absent from the dimension arms -- kept the bare
`value` label while its arm siblings took `value_0`. It now shares the
arm's suffix. Single-arm labels keep the per-occurrence rule, so unsplit
fields and same-name bounds pairs (confidence, cartography) are
unchanged.

Regenerate pyspark output: the vehicle `value` integer check on
access_restrictions, prohibited_transitions, and speed_limits reports
`value_0`.

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
Arm-grouped label suffixing cannot distinguish two same-name checks
landing in one arm of a split union field. No current schema produces
that shape; if one ever does, generation now raises instead of emitting
indistinguishable violation labels.

Signed-off-by: Seth Fitzsimmons <seth@mojodna.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

change type - minor 🤏 Minor schema change. See https://lf-overturemaps.atlassian.net/wiki/x/GgDa

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant